using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Xml.Linq;

namespace WB.Mvc.Forms
{
	public delegate object MemberAccess<T>(T obj);

	[Serializable]
	public class ModelForm : FormBase
	{
		public const string primaryKeyInputId = "__MODELFORM_PRIMARYKEY__";
		public const string typeInputId = "__MODELFORM_TYPE__";

		private object model;
		private Predicate<PropertyDescriptor> shouldIncludeProperty = descriptor => true;
		private readonly Type modelType;
		private readonly IList<IFieldStrategy> strategies;
		private IDictionary<FieldGroup, object> fieldGroupsToModels;
		private bool fieldsAreInitialized = false;

		#region .ctor

		public ModelForm(Type modelType, IEnumerable<IFieldStrategy> strategies) : base(modelType.Name.ToHumanReadable())
		{
			this.modelType = modelType;

			this.strategies = strategies.ToList();
			RegisterDefaultStrategies();

			FieldGroupStrategyProvider = new ModelFieldGroupStrategyProvider(this.strategies.ToArray());
		}

		public ModelForm(object model) : this(model, Enumerable.Empty<IFieldStrategy>())
		{
		}

		public ModelForm(object model, IEnumerable<IFieldStrategy> strategies) : this(model.GetType(), strategies)
		{
			Model = model;
		}

		#endregion

		/// <summary>
		/// Source of strategies for creating instances of <see cref="FieldGroup"/> from a model.
		/// </summary>
		public IFieldGroupStrategyProvider FieldGroupStrategyProvider { get; set; }

		public object Model
		{
			get { return model; }
			set
			{
				if (!modelType.IsAssignableFrom(value.GetType()))
					throw new ArgumentException(string.Format("Expected a model of type {0}.", modelType));
				if (model != null)
				{
					FieldGroups.Clear();
					model = value;
					fieldsAreInitialized = false;
				}
				model = value;
			}
		}

		public Type ModelType
		{
			get { return modelType; }
		}

		/// <summary>
		/// Get or set the unique identifier of model associated with this form.
		/// </summary>
		public object UniqueIdentifier { get; set; }

		public IDictionary<FieldGroup, object> FieldGroupsToModels
		{
			get { return fieldGroupsToModels; }
		}

		/// <summary>
		/// A predicate that determines whether a given property descriptor should be included in this model form.
		/// </summary>
		public virtual Predicate<PropertyDescriptor> ShouldIncludeProperty
		{
			get { return shouldIncludeProperty; }
			set { shouldIncludeProperty = value; }
		}

		public override void Load(NameValueCollection form)
		{
			if (Model == null)
				throw new InvalidOperationException("Model is null.  Must have a model before calling Load.");

			if (form != null)
			{
				InitializeFields();

				// Load the values from the form into the Fields collection.
				base.Load(form);

				if (IsValid())
				{
					// Save the fields values to the model.
					SaveModel();
				}
			}
		}

		/// <summary>
		/// Persists data in <see cref="ModelForm{T}.Fields"/> to the member of <see cref="model"/>
		/// that they map to.
		/// </summary>
		protected virtual void SaveModel()
		{
			if (!IsValid())
				throw new InvalidOperationException("Form date is in an invalid state.");
			
			if (Model == null)
				throw new InvalidOperationException("Model is null.  Must have a model before calling SaveModel.");

			foreach (ModelFieldGroup group in FieldGroups.OfType<ModelFieldGroup>())
			{
				group.UpdateModel(FieldGroupsToModels[group]);
			}
		}

		/// <summary>
		/// Gets the primary key from <see cref="ModelForm.Model"/>.
		/// </summary>
		protected virtual PropertyDescriptor GetUniqueIdentifierDescriptor()
		{
			PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(Model);
			foreach (PropertyDescriptor property in properties)
			{
				if (property.Name == "Id")
					return property;
			}

			throw new InvalidOperationException("Could not find primary key for model.");
		}

		protected override XElement Render(Func<XElement[], XElement> fieldGroupFactory,
		                                   Func<XElement[], XElement> fieldFactory)
		{
			if (!fieldsAreInitialized)
				InitializeFields();

			XElement element = base.Render(fieldGroupFactory, fieldFactory);

			PropertyDescriptor uniqueIdentifierDescriptor = GetUniqueIdentifierDescriptor();
			var uniqueIdentifier = uniqueIdentifierDescriptor.GetValue(Model);
			bool isTransient = uniqueIdentifier.Equals(Activator.CreateInstance(uniqueIdentifierDescriptor.PropertyType));

			// Add a hidden field containing the unique identifier of the model associated with this form.
			// TODO: Encrypt these fields.
			if (!isTransient)
			{
				element.Add(
					new XElement("input",
					             new XAttribute("id", primaryKeyInputId),
					             new XAttribute("name", primaryKeyInputId),
					             new XAttribute("type", "hidden"),
					             new XAttribute("value", uniqueIdentifier)));
			}
			element.Add(
				new XElement("input", 
					new XAttribute("id", typeInputId),
					new XAttribute("name", typeInputId),
					new XAttribute("type", "hidden"),
					new XAttribute("value", GetType().AssemblyQualifiedName)));

			return element;
		}

		protected virtual void InitializeFields()
		{
			IFieldGroupStrategy strategy = FieldGroupStrategyProvider.GetStrategy(model);
			fieldGroupsToModels = strategy.Create(model, ShouldIncludeProperty);

			FieldGroups.Clear();
			FieldGroupsToModels.ToList().ForEach(x => FieldGroups.Add(x.Key));

			fieldsAreInitialized = true;
		}

		private void RegisterDefaultStrategies()
		{
			strategies.Add(new TextAreaConventionFieldStrategy());
			strategies.Add(new EnumToRadioButtonListFieldStrategy());
			strategies.Add(new StringToCharFieldStrategy());
			strategies.Add(new IntToIntegerFieldStrategy());
			strategies.Add(new BoolToBooleanFieldStrategy());
		}
	}

	[Serializable]
	public class ModelForm<T> : ModelForm
		where T : class
	{
		/// <summary>
		/// Creates an instance of <see cref="ModelForm{T}"/>.
		/// </summary>
		public ModelForm() : base(typeof(T), Enumerable.Empty<IFieldStrategy>())
		{
		}

		/// <summary>
		/// Creates an instance of <see cref="ModelForm{T}"/>.
		/// </summary>
		public ModelForm(T model)
			: this(model, Enumerable.Empty<IFieldStrategy>())
		{
		}

		/// <summary>
		/// Creates an instance of <see cref="ModelForm{T}"/>.
		/// </summary>
		/// <param name="strategies">
		/// Custom strategies to be used.  These strategies are 
		/// checked before the default strategies.  If a strategy does not wish to handle
		/// a parameter, it should return <c>null</c>.
		/// </param>
		/// <remarks>
		/// Default field strategies are provided.  The strategies parameter
		/// can be used to override the default behavior.
		/// </remarks>
		public ModelForm(T model, IEnumerable<IFieldStrategy> strategies, params string[] fieldsToIgnore)
			: base(model, strategies)
		{
		}

		public new T Model
		{
			get { return (T) base.Model; }
			set { base.Model = value; }
		}

		public T GetModel()
		{
			return Model;
		}
	}
}